﻿using System;
using System.IO;
using System.Threading;
using System.Net.Sockets;
using System.Net;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using ECE454P1;
using main_test;

namespace ECE454P1
{

    public enum message_type { data, message }
    public enum State { connected, disconnected, unknown }
    
    [Serializable]
    struct ipc_message
    {
        public IPAddress ip_source;
        public message_type message_type;
        public string message;
        public string file_name;
        public string file_hash;
        public int number_of_chunks;
        public int chunk_number;
    }

    class TcpListen
    {
        private TcpListener server;
        private Thread server_thread;
        private Peers peers;
        private Status status;

        public TcpListen(ref Peers peers, ref Status status)
        {
            this.status = status;
            this.peers = peers;
            this.server = new TcpListener(IPAddress.Any, 10032);
            this.server_thread = new Thread(new ThreadStart(listen_thread));
            this.server_thread.Start();
        }

        public void listen_thread()
        {
            this.server.Start();
            while (true)
            {
                TcpClient client = this.server.AcceptTcpClient();
                Console.WriteLine("TCP connection accepted");

                Thread client_thread = new Thread(new ParameterizedThreadStart(communication_thread));
                client_thread.Start(client);
            }
        }

        private void communication_thread(object tcp_client)
        {
            TcpClient client = (TcpClient)tcp_client;
            NetworkStream stream = client.GetStream();

            int i = 0;
            Byte[] r_message = new Byte[Constants.chunkSize];
            IFormatter formatter = new BinaryFormatter();
            int bytes_read = 0;

            //Read header

            do
            {
                i = stream.Read(r_message, bytes_read, r_message.Length - bytes_read);
                bytes_read += i;
                Console.WriteLine("SERVER: Bytes Read:{0}", bytes_read);
                Thread.Sleep(500);
            } while (stream.DataAvailable);
            

            Console.WriteLine("SERVER: Read Message");

            MemoryStream ms = new MemoryStream(r_message);
            ipc_message header_message = (ipc_message)formatter.Deserialize(ms);

            Console.WriteLine("SERVER: byte gonna send");
            stream.WriteByte(00);
            Console.WriteLine("SERVER: byte sent");
            //Message type
            if (header_message.message_type == message_type.message)
            {
                Console.WriteLine("SERVER: message:{0}", header_message.message);
                if (header_message.message == "join")
                {
                    foreach (Peer p in this.peers.peers)
                    {
                        if (header_message.ip_source.Equals(p.ipaddress))
                        {
                            Console.WriteLine("CHANGING STATE {0} to {1}", p.state.ToString(), State.connected.ToString());
                            p.state = State.connected;
                            break;
                        }
                    }
                }
                else if (header_message.message == "leave")
                {
                    foreach (Peer p in this.peers.peers)
                    {
                        if (header_message.ip_source.Equals(p.ipaddress))
                        {
                            Console.WriteLine("CHANGING STATE {0} to {1}", p.state.ToString(), State.disconnected.ToString());
                            p.state = State.disconnected;
                            break;
                        }
                    }
                }
                else if (header_message.message == "query")
                {
                    formatter.Serialize(stream, this.status);
                }
            }
            //Data type
            else if (header_message.message_type == message_type.data)
            {
                Console.WriteLine("SERVER: data type");
                string[] file_info = header_message.file_name.Split('.');
                FileStream file_stream = new FileStream("files/partial_chunks/" + header_message.file_hash + "_" + file_info[0] + "_" + header_message.chunk_number + "_" + file_info[1], FileMode.Create, FileAccess.ReadWrite);

                while ((i = stream.Read(r_message, 0, r_message.Length)) != 0)
                {
                    file_stream.Write(r_message, 0, i);
                    Console.WriteLine("SERVER: Recieved:{0}", i);
                }
                
                Console.WriteLine("SERVER: Finished Receiving file", i);
                file_stream.Close();
            }
            stream.Close();
            client.Close();
            return;
        }
    }

    // Peer and Status are the classes we really care about
    // Peers is a container; feel free to do a different container
    class Peer
    {
        public uint port;
        public IPAddress ipaddress;
        public Peers peers;
        public State state;
        public Status status;
        public Thread thread;

        private TcpClient _client;
        private NetworkStream _network_stream;
        private bool _is_connected = false;

        public int insert(string fileName)
        {
            if (File.Exists(fileName))
            {
                string insert_hash = md5_hash(fileName);

                foreach (string hash in this.status.file_hash)
                {
                    if (hash == insert_hash)
                    {
                        Console.WriteLine("INSERT: File Already Exists");
                        return Constants.errOK;
                    }
                }

                FileInfo file = new FileInfo(fileName);
                if(File.Exists("files/"+fileName))
                {
                    int i = 0;
                    while (File.Exists("files/" + Path.GetFileNameWithoutExtension(file.Name) + "_" + i + file.Extension))
                    {
                        i++;
                    }
                    File.Copy(fileName, "files/" + Path.GetFileNameWithoutExtension(file.Name) + "_" + i + file.Extension);
                }
                else
                {
                    File.Copy(fileName, "files/" + file.Name);
                }
                return Constants.errOK;
            }
            return Constants.errUnknownFatal;
        }

        public int query(ref Status status)
        {
            foreach (Peer p in this.peers.peers)
            {
                if (p != this)
                {
                    if (p.connect() == Constants.errCannotConnect)
                    {
                        p.state = State.disconnected;
                    }
                    else
                    {
                        p.state = State.connected;

                        ipc_message header_message = new ipc_message();
                        header_message.message = "query";
                        header_message.ip_source = this.ipaddress;

                        p.send_query(header_message);
                    }
                }
            }
            return 1;
        }

        public int join()
        {
            //join
            
            foreach (Peer p in this.peers.peers)
            {
                if (p != this)
                {
                    if (p.connect() == Constants.errCannotConnect)
                    {
                        p.state = State.disconnected;
                    }
                    else
                    {
                        p.state = State.connected;

                        ipc_message header_message = new ipc_message();
                        header_message.message = "join";
                        header_message.ip_source = this.ipaddress;

                        p.send_message(header_message);
                    }
                }
            }
            
            
            //Send files
            for (int i = 0; i < status.numberOfFiles(); i++)
            {
                for (int c = 0; c < status.number_of_chunks[i]; c++)
                {
                    foreach(Peer p in this.peers.peers)
                    {
                        if (p != this && p.state == State.connected)
                        {
                            if (p.thread != null)
                            {
                                while (p.thread.IsAlive) ;
                            }
                            p.thread = new Thread(new ParameterizedThreadStart(p.send_chunk));
                            ipc_message header_message = new ipc_message();
                            header_message.file_name = status.file_name[i];
                            header_message.file_hash = status.file_hash[i];
                            header_message.chunk_number = c;

                            p.thread.Start(header_message);
                            
                        }
                    }
                }
            }
            

            return Constants.errUnknownFatal; // Unknown error
        }

        public int leave()
        {
            foreach (Peer p in this.peers.peers)
            {
                if (p != this && p.state == State.connected)
                {
                    ipc_message header_message = new ipc_message();
                    header_message.message = "leave";
                    header_message.ip_source = this.ipaddress;

                    if (p.thread != null)
                    {
                        while (p.thread.IsAlive) ;
                    }

                    p.send_message(header_message);
                }
            }
            
            return Constants.errOK;
        }

        public void send_message(object message)
        {
            ipc_message header_message = (ipc_message)message;
            Byte[] chunk = new byte[Constants.chunkSize];

            if (!is_connected())
            {
                connect();
            }
            header_message.message_type = message_type.message;
            
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(_network_stream, header_message);

            Console.WriteLine("CLIENT: Waiting for Byte");
            _network_stream.ReadByte();

            disconnect();
            
        }

        public void send_query(object message)
        {
            int i = 0;
            int bytes_read = 0;
            ipc_message header_message = (ipc_message)message;
            Byte[] r_message = new byte[Constants.chunkSize];

            if (!is_connected())
            {
                connect();
            }
            header_message.message_type = message_type.message;

            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(_network_stream, header_message);

            Console.WriteLine("CLIENT: Waiting for Byte");
            _network_stream.ReadByte();

            do
            {
                i = _network_stream.Read(r_message, bytes_read, r_message.Length - bytes_read);
                bytes_read += i;
            } while (i!=0);

            MemoryStream ms = new MemoryStream(r_message);
            this.status = (Status)formatter.Deserialize(ms);
            
            disconnect();
        }

        public void send_chunk(object message)
        {
            ipc_message header_message = (ipc_message)message;
            Byte[] chunk = new byte[Constants.chunkSize];

            if (!is_connected())
            {
                connect();
            }

            header_message.ip_source = this.ipaddress;
            header_message.message_type = message_type.data;

            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(_network_stream, header_message);

            Console.WriteLine("CLIENT: Waiting for Byte");
            _network_stream.ReadByte();
            Console.WriteLine("CLIENT: Byte Received");

            FileStream file_stream = new FileStream(header_message.file_name, FileMode.Open, FileAccess.Read, FileShare.Read);

            int i = 0;
            for (int c = header_message.chunk_number; c >= 0; c--)
            {
                i = file_stream.Read(chunk, 0, Constants.chunkSize);
            }

            Console.WriteLine("CLIENT: Sending Chunk: {0}",header_message.chunk_number);
            _network_stream.Write(chunk, 0, i);
            Console.WriteLine("CLIENT: Chunk Size: {0}", i);

            Console.WriteLine("CLIENT: Chunk Sent");
            file_stream.Close();
            disconnect();
        }

        public void send_file(object file_name)
        {
            string file = (string)file_name;
            if (!is_connected())
            {
                connect();
            }
            ipc_message header_message = new ipc_message();
            Byte[] chunk = new byte[Constants.chunkSize];

            header_message.ip_source = this.ipaddress;
            header_message.message_type = message_type.data;
            header_message.file_name = file;

            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(_network_stream, header_message);

            Console.WriteLine("CLIENT: Waiting for Byte");
            _network_stream.ReadByte();
            Console.WriteLine("CLIENT: Byte Received");

            FileStream file_stream = new FileStream(file, FileMode.Open, FileAccess.Read,FileShare.Read);

            int i = 0;
            Console.WriteLine("CLIENT: Sending File");
            while ((i = file_stream.Read(chunk, 0, Constants.chunkSize)) != 0)
            {
                _network_stream.Write(chunk, 0, i);
                Console.WriteLine("CLIENT: Chunk:{0}", i);
            }
            Console.WriteLine("CLIENT: File Sent");
            file_stream.Close();
            disconnect();

            return ;
        }

        private bool is_connected()
        {
            return _is_connected;
        }

        private int disconnect()
        {
            _network_stream.Close();
            _client.Close();
            _is_connected = false;

            return 1;
        }

        private int connect()
        {
            try
            {
                _client = new TcpClient(ipaddress.ToString(), (int)port);
                
            }
            catch (ArgumentException e)
            {
                Console.WriteLine("Argument Exception: {0}", e);
                return Constants.errCannotConnect; // Cannot connect to anything; fatal error
            }
            catch (SocketException e)
            {
                Console.WriteLine("Socket Exception, Server offline: {0}",e);
                return Constants.errCannotConnect; // Cannot connect to anything; fatal error
            }
            _network_stream = _client.GetStream();
            _is_connected = true;
            return Constants.errOK;
        }

        public string md5_hash(string file)
        {
            FileStream file_stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] md5_hash = md5.ComputeHash(file_stream);
            file_stream.Close();

            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < md5_hash.Length; i++)
            {
                sb.Append(md5_hash[i].ToString("x2"));
            }

            return sb.ToString();
        }
    }

    // Status is the class that you populate with status data on the state
    // of replication in this peer and its knowledge of the replication
    // level within the system.
    // The thing required in the Status object is the data as specified in the private section
    // The methods shown are examples of methods that we may implement to access such data
    // You will need to create methods to populate the Status data.
    [Serializable]
    class Status
    {
        private int _numFiles = 0;                                                          // The number of files currently in the system, as viewed by this peer
        private float[] _local = new float[Constants.maxFiles];                         // The fraction of the file present locally 
        // (= chunks on this peer/total number chunks in the file)
        private float[] _system = new float[Constants.maxFiles];                        // The fraction of the file present in the system 
        // (= chunks in the system/total number chunks in the file)
        // (Note that if a chunk is present twice, it doesn't get counted twice;
        // this is simply intended to find out if we have the whole file in
        // the system; given that a file must be added at a peer, think about why
        // this number would ever not be 1.)
        private int[] _leastReplication = new int[Constants.maxFiles];                  // Sum by chunk over all peers; the minimum of this number is the least 
        // replicated chunk, and thus represents the least level of replication
        // of the file
        private float[] _weightedLeastReplication = new float[Constants.maxFiles];      // Sum all chunks in all peers; divde this by the number of chunks in the
        // file; this is the average level of replication of the file
        public string[] file_hash = new string[Constants.maxFiles];
        public string[] file_name = new string[Constants.maxFiles];
        public int[] number_of_chunks = new int[Constants.maxFiles];
        public long[] file_size = new long[Constants.maxFiles];
        public List<int>[] chunks_present = new List<int>[Constants.maxFiles];

        private FileInfo[] getFiles()
        {
            return new DirectoryInfo("files").GetFiles();
        }

        public Status()
        {
            int i = 0;
            int c = 0;
            int match = 0;
            DirectoryInfo di = new DirectoryInfo("files");
            FileInfo[] fi = di.GetFiles();

            foreach (FileInfo file in fi)
            {
                _local[i] = 1;
                file_hash[i] = Constants.md5_hash(file.FullName);
                file_name[i] = file.Name;
                file_size[i] = file.Length;
                number_of_chunks[i] = (int)Math.Ceiling( ((double)file.Length) / ((double)Constants.chunkSize) );
                chunks_present[i] = new List<int>();
                Console.WriteLine("File has {0} Chunks",number_of_chunks[i]);
                for(c = 0; c < number_of_chunks[i];c++)
                {
                    chunks_present[i].Add(c);
                    Console.WriteLine("Adding chunk {0}",c);
                }
                Console.WriteLine("SETUP: FILE HASH: {0}",file_hash[i]);
                _numFiles++;
                i++;
            }

            di = new DirectoryInfo("files/partial_chunks");
            fi = di.GetFiles();

            foreach (FileInfo file in fi)
            {
                for (c = 0; c < _numFiles; c++)
                {
                    if (file.Name.Contains(file_hash[c]))
                    {
                        match = 1;
                        if(!chunks_present[c].Contains(Convert.ToInt32(file.Name.Split('_')[2])))
                        {
                            chunks_present[c].Add(Convert.ToInt32(file.Name.Split('_')[2]));
                            Console.WriteLine("CHUNKS PRESENT1:{0}", Convert.ToInt32(file.Name.Split('_')[2]));
                        }
                        break;
                    }
                }
                if (match != 1)
                {
                    file_hash[i] = file.Name.Split('_')[0];
                    Console.WriteLine("FILE HASH:{0}", file.Name.Split('_')[0]);
                    file_name[i] = file.Name.Split('_')[1];
                    Console.WriteLine("FILE NAME:{0}", file.Name.Split('_')[1]);
                    chunks_present[i] = new List<int>();
                    chunks_present[i].Add(Convert.ToInt32(file.Name.Split('_')[2]));
                    Console.WriteLine("CHUNKS PRESENT:{0}", Convert.ToInt32(file.Name.Split('_')[2]));
                    i++;
					_numFiles++;
                    match = 0;
                }
            }
        }

        public int update()
        {
            int i = 0;
            foreach (FileInfo file in getFiles())
            {
                _local[i] = 1;
                file_hash[i] = Constants.md5_hash(file.FullName);
                number_of_chunks[i] = (int)Math.Ceiling(((double)file.Length) / ((double)Constants.chunkSize));
                Console.WriteLine("SETUP: FILE HASH: {0}", file_hash[i]);
                i++;
            }
            return 1;
        }

        public int populate(Peer peer)
        {
            // TODO
            return Constants.errOK;
        }

        public int numberOfFiles()
        {
            _numFiles = getFiles().Length;
            Console.WriteLine("Number of files: " + _numFiles);
            return _numFiles;
        }

        // Use -1 to indicate if the file requested is not present
        public float fractionPresentLocally(int fileNumber)
        {
            if (fileNumber >= numberOfFiles())
            {
                return Constants.errUnknownFatal;
            }
            return _local[fileNumber];
        }

        // Use -1 to indicate if the file requested is not present in the system
        public float fractionPresent(int fileNumber)
        {
            if (fileNumber >= numberOfFiles())
            {
                return Constants.errUnknownFatal;
            }
            return _system[fileNumber];
        }

        // Use -1 to indicate if the file requested is not present in the system
        public int minimumReplicationLevel(int fileNumber)
        {
            if (fileNumber >= numberOfFiles())
            {
                return Constants.errUnknownFatal;
            }
            return _leastReplication[fileNumber];
        }

        // Use -1 to indicate if the file requested is not present in the system
        public float averageReplicationLevel(int fileNumber)
        {
            if (fileNumber >= numberOfFiles())
            {
                return Constants.errUnknownFatal;
            }
            return _weightedLeastReplication[fileNumber];
        }
    }


    // Peers is a dumb container to hold the peers; the number of peers is fixed,
    // but needs to be set up when a peer starts up; feel free to use some other
    // container class, but that class must have a method that allows it to read
    // the peersFile, since otherwise you have no way of having a calling entity 
    // tell your code what the peers are in the system.
    class Peers
    {
        public List<Peer> peers = new List<Peer>();
        private int _numPeers = 0;

        // The peersFile is the name of a file that contains a list of the peers
        // Its format is as follows: in plaintext there are up to maxPeers lines,
        // where each line is of the form: <IP address> <port number>
        // This file should be available on every machine on which a peer is started,
        // though you should exit gracefully if it is absent or incorrectly formatted.
        // After execution of this method, the _peers should be present.
        public int initialize(string peersFile)
        {
            if (File.Exists(peersFile))
            {
                try
                {
                    StreamReader file = File.OpenText(peersFile);
                    string s = "";
                    string[] server;

                    while ((s = file.ReadLine()) != null)
                    {
                        server = s.Split(',');
                        try
                        {
                            Peer peer = new Peer();
                            peer.ipaddress = IPAddress.Parse(server[0]);
                            peer.port = uint.Parse(server[1]);
                            peer.peers = this;
                            peer.state = State.unknown;

                            peers.Add(peer);
                            
                            _numPeers++;
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine("PEERS: IP Address Exception: {0}", e);
                        }
                    }

                    file.Close();
                }
                catch (Exception e)
                {
                    Console.WriteLine("PEERS: File Open Exception: {0}", e);
                }
            }
            else
            {
                Console.WriteLine("File does not exist");
                return Constants.errUnknownFatal;
            }
            return Constants.errUnknownFatal;
        }
    }

    class Constants
    {
        public const int maxPeers = 6;
        //65536
        public const int chunkSize = 65536;
        public const int maxFiles = 100;

        public const int errOK = 0; // Everything good
        public const int errUnknownWarning = 1; // Unknown warning
        public const int errUnknownFatal = -2; // Unknown error
        public const int errCannotConnect = -3; // Cannot connect to anything; fatal error
        public const int errNoPeersFound = -4; // Cannot find any peer (e.g., no peers in a peer file); fatal
        public const int errPeerNotFound = 5; // Cannot find some peer; warning, since others may be connectable

        public static string md5_hash(string file)
        {
            FileStream file_stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] md5_hash = md5.ComputeHash(file_stream);
            file_stream.Close();

            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < md5_hash.Length; i++)
            {
                sb.Append(md5_hash[i].ToString("x2"));
            }

            return sb.ToString();
        }
    }
}

namespace main_test
{
    class MainClass
    {
        public static void Main(string[] args)
        {
            Peers peers = new Peers();
            Status status = new Status();
            new TcpListen(ref peers, ref status);
            IPAddress local_address = IPAddress.Parse("127.0.0.1");
            Peer local_peer = new Peer();

            foreach (IPAddress ip in Dns.GetHostEntry(Dns.GetHostName()).AddressList)
            {
                if (ip.AddressFamily == AddressFamily.InterNetwork)
                {
                    local_address = ip;
                }
            }

            peers.initialize(@"peers.txt");

            foreach (Peer p in peers.peers)
            {
                Console.WriteLine(p.ipaddress.ToString());

                if (IPAddress.Equals(p.ipaddress, local_address))
                {
                    local_peer = p;
                    break;
                }
            }

            //status.numberOfFiles();
            local_peer.status = status;
            //local_peer.insert(@"test.zip");
            while (true)
            {
                Thread.Sleep(1);
            }
        }
    }
}

